Option Explicit

' ======================================================================
' 可配置属性
' ======================================================================
' 对象属性的物理名称在 table 中的行号(从 1 开始，这里是 table 定义的起始位置开始计算的行号)
Public fieldPhyNameRowNo As Integer
' 对象属性的类型名称在 table 中的行号(从 1 开始，这里是 table 定义的起始位置开始计算的行号)
Public fieldTypeRowNo As Integer
' 第一行数据在 table 中的行号 (从 1 开始，这里是 table 定义的起始位置开始计算的行号)
Public tableDataStartRowNo As Integer

' ======================================================================
' 构造器
' ======================================================================
Private Sub Class_Initialize()
    ' 对象属性的物理名称在 table 中的行号(从 1 开始，这里是 table 定义的起始位置开始计算的行号)
    fieldPhyNameRowNo = 1
    ' 对象属性的类型名称在 table 中的行号(从 1 开始，这里是 table 定义的起始位置开始计算的行号)
    fieldTypeRowNo = 2
    ' 第一行数据在 table 中的行号 (从 1 开始，这里是 table 定义的起始位置开始计算的行号)
    tableDataStartRowNo = 3
End Sub

' ======================================================================
' 生成数据的方法
' ======================================================================
' 为 sheet 选中区域中的第一个 table 定义来生成 json 数据
' 如果 table 定义中只有一行数据，则会生成一个对象的字符串
' 如果 table 定义中包含多行数据，则会生成一个对象数组
'
' @param sh 包含 table 定义的 sheet
' @param tableRangeStr table 定义所在的区域
'                      如果参数 dataRangeStr 是一个单元格，则将会以这个单元格为表格的起始位置开始向下扫描 table
'                      如果参数 dataRangeStr 是一个大的区域，则将会以这个区域的起始位置为表格的起始位置，扫描整个区域的数据，并生成 json数据
' @return json 对象或数组的字符串
Public Function createOneTableJsonStr(ByRef sh As Worksheet, ByRef tableRangeStr As String) As String
    ' ==================================================
    ' 1. 初始化边界
    ' ==================================================
    ' ==============================
    ' 1.1. 需要的变量
    ' ==============================
    ' 1. 扫描属性名称行的行号: `scanFieldPhyNameRowNo As Long`
    Dim scanFieldPhyNameRowNo As Long
    ' 2. 扫描属性类型名称行的行号: `scanFieldTypeRowNo As Long`
    Dim scanFieldTypeRowNo As Long
    ' 3. 扫描数据的起始行的行号: `scanDataStartRowNo As Long`
    Dim scanDataStartRowNo As Long
    ' 4. 扫描的结束行号: `scanDataEndRowNo As Long`
    Dim scanDataEndRowNo As Long
    ' 5. 扫描的起始列号: `scanStartColNo As Integer`
    Dim scanStartColNo As Integer
    ' 6. 扫描的结束列号: `scanEndColNo As Integer`
    Dim scanEndColNo As Integer

    ' 获取数据区域内的单元格数量
    Dim tableRange As Range
    Set tableRange = UtilsWorksheet.getRangeByStr(sh, tableRangeStr)

    ' ==============================
    ' 1.2. 如果 `tableRangeStr` 是一个单元格
    ' ==============================
    If tableRange.Count = 1 Then
        ' 1. 设置，扫描属性名称行的行号: `scanFieldPhyNameRowNo` 为 `tableRangeStr 行号 + fieldPhyNameRowNo - 1`
        scanFieldPhyNameRowNo = tableRange.Row + fieldPhyNameRowNo - 1
        ' 2. 设置，扫描属性类型名称行的行号: `scanFieldTypeRowNo` 为 `tableRangeStr 行号 + fieldTypeRowNo - 1`
        scanFieldTypeRowNo = tableRange.Row + fieldTypeRowNo - 1
        ' 3. 设置，扫描数据的起始行的行号: `scanDataStartRowNo` 为 `tableRangeStr 行号 + tableDataStartRowNo - 1`
        scanDataStartRowNo = tableRange.Row + tableDataStartRowNo - 1
        ' 4. 设置，扫描的结束行号: `scanDataEndRowNo` 为 `sh` 中被使用的最后一行的行号
        scanDataEndRowNo = UtilsWorksheet.getUsedRangeEndRowNo(sh)
        ' 5. 设置，扫描的起始列号: `scanStartColNo` 为 `tableRangeStr` 的列号
        scanStartColNo = tableRange.Column
        ' 6. 设置，扫描的结束列号: `scanEndColNo` 为 `sh` 中被使用的最后一列的列号
        scanEndColNo = UtilsWorksheet.getUsedRangeEndColumnNo(sh)
    Else
    ' ==============================
    ' 1.3. 否则，`dataRangeStr` 是一个大的区域
    ' ==============================
        ' 1. 设置，扫描属性名称行的行号: `scanFieldPhyNameRowNo` 为 `tableRangeStr 行号 + fieldPhyNameRowNo - 1`
        scanFieldPhyNameRowNo = tableRange.Row + fieldPhyNameRowNo - 1
        ' 2. 设置，扫描属性类型名称行的行号: `scanFieldTypeRowNo` 为 `tableRangeStr 行号 + fieldTypeRowNo - 1`
        scanFieldTypeRowNo = tableRange.Row + fieldTypeRowNo - 1
        ' 3. 设置，扫描数据的起始行的行号: `scanDataStartRowNo` 为 `tableRangeStr 行号 + tableDataStartRowNo - 1`
        scanDataStartRowNo = tableRange.Row + tableDataStartRowNo - 1
        ' 4. 设置，扫描的结束行号: `scanDataEndRowNo` 为 `tableRangeStr` 的结束行号
        scanDataEndRowNo = UtilsWorksheet.getRangeLastRowNo(tableRange)
        ' 5. 设置，扫描的起始列号: `scanStartColNo` 为 `tableRangeStr` 的列号
        scanStartColNo = tableRange.Column
        ' 6. 设置，扫描的结束列号: `scanEndColNo` 为 `tableRangeStr` 的结束列号
        scanEndColNo = UtilsWorksheet.getRangeLastColNo(tableRange)
    End If

    ' ==================================================
    ' 2. 初始化变量
    ' ==================================================
    ' 1. 对象属性数量 `fieldCount As Integer`
    Dim fieldCount As Integer
    ' 2. 行索引 `ri As Long`
    Dim ri As Long
    ' 3. 列索引 `ci As Integer`
    Dim ci As Integer
    ' 4. 行数 `dataCount As Integer`
    Dim dataCount As Integer
    ' 5. 结束字符串 `result As String`
    Dim result As String
    ' 6. 对象字符串前缀 `jsonObjPrefix As String`
    '    如果是多行，则需要保存一个单位的空格
    Dim jsonObjPrefix As String
    ' 7. 属性字符串前缀 `jsonFieldPrefix As String`
    Dim jsonFieldPrefix As String
    ' 8. 遍历时的属性数据字符串 `fieldDataStr As String`
    Dim fieldDataStr As String
    ' 9. 遍历时的属性物理名字符串 `fieldPhyNameStr As String`
    Dim fieldPhyNameStr As String
    ' 10. 临时字符串 `temp As String`
    Dim temp As String

    ' ==================================================
    '3. 计算对象属性数量
    ' ==================================================
    ' 1. 计算对象属性数量，并保存到变量 `fieldCount`
    fieldCount = UtilsWorksheet.getConsecutiveNoEmptyColCount(sh, scanFieldPhyNameRowNo, scanStartColNo, scanEndColNo)
    ' 2. 检查: 如果不存在属性 `fieldCount == 0`，则停止，并返回空字符串
    If fieldCount = 0 Then
        Exit Function
    End If
    ' 3. 重设: 扫描的结束列号: `scanEndColNo` 为 `scanStartColNo + fieldCount - 1`
    scanEndColNo = scanStartColNo + fieldCount - 1

    ' ==================================================
    ' 4. 计算数据的行数
    ' ==================================================
    ' 1. 遍历计算数据行数
    ' 1.1. 从 `ri = scanDataStartRowNo` 开始遍历到 `scanDataEndRowNo`
    For ri = scanDataStartRowNo To scanDataEndRowNo
        ' 1.2. 检查，如果第 `ri` 行是空行，则停止遍历
        If UtilsWorksheet.hasNoEmptyCellInRowRange(sh, ri, scanStartColNo, scanEndColNo) = False Then
            Exit For
        End If

        ' 1.3. `dataCount++`
        dataCount = dataCount + 1

        ' 1.4. 回到 2 继续遍历
    Next

    ' 2. 检查: 如果 `dataCount == 0`，则停止，并返回空字符串
    If dataCount = 0 Then
        Exit Function
    End If
    
    ' 3. 重设: 扫描的结束行号 `scanDataEndRowNo` 为 `scanDataStartRowNo + dataCount - 1`
    scanDataEndRowNo = scanDataStartRowNo + dataCount - 1

    ' ==================================================
    ' 5. 收集属性的物理名称集合: `fieldPhyNameList`
    ' ==================================================
    Dim fieldPhyNameStrList As Collection
    Set fieldPhyNameStrList = getFieldPhyNameStrList(sh, _
        scanFieldPhyNameRowNo, _
        scanStartColNo, scanEndColNo)

    ' ==================================================
    ' 6. 收集属性数据的边界字符集合: `fieldDataWrapperStrList`
    ' ==================================================
    Dim fieldDataWrapperStrList As Collection
    Set fieldDataWrapperStrList = getFieldDataWrapperStrList(sh, _
        scanFieldTypeRowNo, _
        scanStartColNo, scanEndColNo)

    ' ==================================================
    ' 7. 初始化各个字符串
    ' ==================================================
    If dataCount > 1 Then
        ' 1. 检查: 如果是多行数据 `dataCount > 1`
        ' 1.1. 设置 `result = "[" + vbCrLf`
        '      这里表示: 有多行数据时，增加数组的边界符号 `[]`，并且在 `[` 后面附加一个换行
        result = "[" & vbCrLf
        ' 1.2. 设置 `jsonObjPrefix = "    "` (4 个空格)
        '      这里表示，生成对象数组时，每个对象前都需要额外附加一个单位的空格
        jsonObjPrefix = "    "
    End If
    ' 2. 否则，`result` 和 `jsonObjPrefix` 全部使用默认值(空字符串)
    ' Noop

    ' 3. 设置: 属性字符串前缀为 `jsonFieldPrefix = jsonObjPrefix + "    "`
    jsonFieldPrefix = jsonObjPrefix & "    "

    ' ==================================================
    ' 8. 生成 json 数据字符串
    ' ==================================================
    With sh
        ' 1. 从 `ri = scanDataStartRowNo` 开始到 `scanDataEndRowNo`，遍历每一行
        For ri = scanDataStartRowNo To scanDataEndRowNo
            ' 2. 连接当前这个对象的起始字符串: `result += jsonObjPrefix + "{" & vbCrLf`
            result = result & jsonObjPrefix & "{" & vbCrLf

            ' 3. 从 `ci = 1` 开始到 `fieldCount` 遍历这一行的每一列
            For ci = 1 To fieldCount
                ' 3.1. 从属性物理名称集合 `fieldPhyNameStrList` 中获取这一列的属性物理名 `fieldPhyNameStr`
                fieldPhyNameStr = fieldPhyNameStrList.Item(ci)

                ' 3.2. 从属性数据边界字符集合 `fieldDataWrapperStrList` 中获取这一列的边界字符 `temp`
                temp = fieldDataWrapperStrList.Item(ci)

                ' 3.3. 获取这一列的属性数据 `fieldDataStr = Cells(ri, scanStartColNo + ci - 1)`
                fieldDataStr = .Cells(ri, scanStartColNo + ci - 1)

                ' 3.4. 连接属性的物理名 + 数据到结果中
                ' 3.4.1. 检查: 如果数据边界字符是空字符串 `temp == ""`，则连接:
                If temp = "" Then
                    result = result & jsonFieldPrefix & fieldPhyNameStr & fieldDataStr
                Else
                ' 3.4.2. 否则，连接:
                    result = result & jsonFieldPrefix & fieldPhyNameStr & temp & fieldDataStr & temp
                End If

                ' 3.5. 检查，如果不是最后一个属性: `ci < fieldCount`，则在末尾连接 `,`: `result += ","`
                If ci < fieldCount Then
                    result = result & ","
                End If

                ' 3.6. 在末尾连接换行符: `result += vbCrLf`
                result = result & vbCrLf
            Next

            ' 4. 这一行数据结束，添加对象的结束字符: `result += jsonObjPrefix + "}"`
            result = result & jsonObjPrefix & "}"

            ' 5. 检查: 如果不是最后一行 `ri < scanDataEndRowNo`，则添加逗号和换行符: `result += "," + vbCrLf`
            If ri < scanDataEndRowNo Then
                result = result & "," & vbCrLf
            End If
        Next
    End With

    ' ==================================================
    ' 9. 检查: 如果是多行数据 `dataCount > 1`，则附加数组的结束字符: `result += vbCrLf + "]"`
    ' ==================================================
    If dataCount > 1 Then
        result = result & vbCrLf & "]"
    End If

    ' ==================================================
    ' 10. 结束，返回 `result`
    ' ==================================================
    createOneTableJsonStr = result

End Function

' 收集属性物理名称
Private Function getFieldPhyNameStrList(ByRef sh As Worksheet, _
    ByRef scanRowNo As Long, _
    ByRef startColNo As Integer, ByRef endColNo As Integer) As Collection

    ' ==================================================
    ' 1. 初始化变量
    ' ==================================================
    ' 1. 遍历索引 `ci As Integer`
    Dim ci As Integer
    ' 2. 初始化返回值 `result As Collection`
    Set getFieldPhyNameStrList = New Collection
    ' 3. 临时字符串 `temp As String`
    Dim temp As String

    ' ==================================================
    ' 2. 从 `ci = startColNo` 开始到 `endColNo` 为止，遍历第 `scanRowNo` 行
    ' ==================================================
    With sh
        For ci = startColNo To endColNo
            ' 1. 获取一个物理名 `temp = Cells(scanRowNo, ci)`
            temp = .Cells(scanRowNo, ci)
            ' 2. 将 `"temp": ` 保存到结果中
            '    这里提前连接物理两边的双引号，以减少创建 json 数据时的字符串连接次数
            getFieldPhyNameStrList.Add """" & temp & """: "
            ' 3. 回到 1 继续遍历
        Next
    End With
End Function

' 收集属性数据的边界字符
Private Function getFieldDataWrapperStrList(ByRef sh As Worksheet, _
    ByRef scanRowNo As Long, _
    ByRef startColNo As Integer, ByRef endColNo As Integer) As Collection

    ' ==================================================
    ' 1. 初始化变量
    ' ==================================================
    ' 1. 遍历索引 `ci As Integer`
    Dim ci As Integer
    ' 2. 初始化返回值 `result As Collection`
    Set getFieldDataWrapperStrList = New Collection
    ' 3. 临时字符串 `temp As String`
    Dim temp As String

    ' ==================================================
    ' 2. 从 `ci = startColNo` 开始到 `endColNo` 为止，遍历第 `scanRowNo` 行
    ' ==================================================
    With sh
        For ci = startColNo To endColNo
            ' 1. 获取一个属性类型名 `temp = Cells(scanRowNo, ci)`，并转换成小写字符
            temp = LCase(Trim(.Cells(scanRowNo, ci)))
            
            ' 2. 检查: 如果 `temp` 是字符串 `temp == "string"`，则保存 `"`
            If temp = "string" Then
                ' 这里只对字符串类型数据的两边添加 `""`，其他类型则忽略
                getFieldDataWrapperStrList.Add """"
            Else
            ' 3. 否则，保存一个空字符串 `''`
                getFieldDataWrapperStrList.Add ""
            End If

            ' 4. 回到 1 继续遍历
        Next
    End With
End Function
